package org.traffometer.traffobase.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

import org.traffometer.common.data.TraffoPacket;
import org.traffometer.opendmtp.protocol.Log;
import org.traffometer.opendmtp.protocol.Packet;
import org.traffometer.traffobase.model.PacketStore;

/**
 * Instances of this class listen on a given network port for incoming TraffoPackets and save them
 * to a PacketStore.
 * 
 * @author Robert S. Brewer
 */
public class PacketListener {

  /**
   * The default interface to bind the server to. 
   */
  protected static final String defaultServerInterface = "127.0.0.1";
  /**
   * The default TCP port number to bind the server to.
   */
  protected static final int defaultServerPort = 31000;

  /**
   * The interface that the server should bind to.
   */
  protected String serverInterface;
  /**
   * The TCP port number the server should bind to.
   */
  protected int port;
  /**
   * The place to store TraffoPackets when we receive them.
   */
  protected PacketStore packetStore;

  /**
   * Prefix for log entries. 
   */
  private static final String LOG_NAME = "SOCKETCONN";

  /**
   * Creates a new PacketListener listening on the provided interface and port, and storing
   * any received TraffoPackets in the provided PacketStore.
   * 
   * @param serverInterface interface to bind the server to (usually an IP address).
   * @param port port to bind the server to (a number between 0 and 65535 inclusive).
   * @param packetStore a properly initialized PacketStore.
   */
  public PacketListener(String serverInterface, int port, PacketStore packetStore) {
    if (serverInterface == null || "".equals(serverInterface)) {
      this.serverInterface = defaultServerInterface;
    }
    else {
      this.serverInterface = serverInterface;
    }
    if (port <= 0 || port > 65535) {
      this.port = defaultServerPort;
    }
    else {
      this.port = port;
    }
    this.packetStore = packetStore;
  }

  /**
   * Creates a new PacketListener listening on the default interface and port, and storing
   * any received TraffoPackets in the provided PacketStore.
   * 
   * @param packetStore a properly initialized PacketStore.
   */
  public PacketListener(PacketStore packetStore) {
    this("", -1, packetStore);
  }

  /**
   * Listens on the configured interface and port for connections from Traffo clients. Note that
   * at present only one TCP connection is handled, at which point the server exits. Also, no
   * attempt has been made to start a new thread for the incoming connection, which would be a
   * good thing.
   */
  public void listen() {
    ServerSocket serverSocket = null;
    Socket socket = null;
    InputStream input;
    OutputStream output;
        
    try {
      serverSocket = new ServerSocket(port, 10, InetAddress.getByName(this.serverInterface));
      System.out.println("Listening on " + this.serverInterface + " on port " + this.port +
          ".");
      socket = serverSocket.accept();
      input = socket.getInputStream();
      output = socket.getOutputStream();
      if ((input == null) || (output == null)) {
        Log.error(LOG_NAME, "Null socket/data");
        return;
      }
      handlePacketStream(input, output);
    }
    catch (IOException e) {
      Log.error(LOG_NAME, "Exception", e);
      return;
    }
    finally {
      try {
        if (serverSocket != null) {
          serverSocket.close();          
        }
        if (socket != null) {
          socket.close();
        }
      }
      catch (IOException e) {
        Log.error(LOG_NAME, "Got error when closing server socket! " + e);
      }
    }
  }

  /**
   * Handles an incoming connection from a client, parsing the OpenDMTP packets in the connection
   * and writing any TraffoPackets to the PacketStore. OpenDMTP packets other than TraffoPackets
   * are accepted but ignored. Any corrupted packets are just read past until the next packet.
   * Only OpenDMTP binary packets are accepted. If the connection terminates prematurely, the
   * method just exits.
   * @param input InputStream from the client.
   * @param output OutputStream to the client.
   */
  public void handlePacketStream(InputStream input, OutputStream output) {
    TraffoPacket traffoPacket;
    int theByte;
    boolean isTraffoPacket = false;
    boolean isAccountIDPacket = false;
    boolean isDeviceIDPacket = false;
    byte[] packetBytes;
    int packetLength = 0;

    try {
      // loop as long as there is data to be read
      for (theByte = input.read(); theByte != -1; theByte = input.read()) {
        // Check for standard header byte
        if (theByte != Packet.HEADER_BASIC) {
          // got something other than OpenDMTP header, just keep looping till we find header
          continue;
        }

        // Check for packet type field
        theByte = input.read();
        switch (theByte) {
        case Packet.PKT_CLIENT_ACCOUNT_ID:
          isAccountIDPacket = true;
          break;
        case Packet.PKT_CLIENT_DEVICE_ID:
          isDeviceIDPacket = true;
          break;
        case Packet.PKT_CLIENT_CUSTOM_FORMAT_0:
          isTraffoPacket = true;
          break;
        case -1:
          Log.error(LOG_NAME, "Got end of data while reading packet type");
          return;
        default:
          // silently ignore all other packet types
          break;
        }

        // Check for packet length field
        packetLength = input.read();
        if (packetLength == -1) {
          Log.error(LOG_NAME, "Got end of data while reading packet length");
          return;
        }
        packetBytes = new byte[packetLength];

        // Slurp up the expected number of payload bytes
        for (int i = 0; i < packetLength; i++) {
          theByte = input.read();
          if (theByte == -1) {
            return;
          }
          packetBytes[i] = (byte) theByte;
        }
        // Now turn those bytes into something
        if (isTraffoPacket) {
          traffoPacket = new TraffoPacket(packetBytes);
          if (traffoPacket != null) {
            System.out.println(traffoPacket);
            packetStore.add(traffoPacket);
          }
        }
        else if (isAccountIDPacket) {
          System.out.println("Received Account ID: " + new String(packetBytes));
        }
        else if (isDeviceIDPacket) {
          System.out.println("Received Device ID: " + new String(packetBytes));
        }
        else {
          continue;          
        }
      }
    }
    catch (IOException ioe) {
      Log.error(LOG_NAME, "Unable to read bytes");
      return;
    }
  }

  /**
   * Getter for packetStore. Setting done only through constructor.
   * 
   * @return the packetStore
   */
  public PacketStore getPacketStore() {
    return packetStore;
  }

  /**
   * Getter for server port. Setting done only through constructor.
   * 
   * @return the port
   */
  public int getPort() {
    return port;
  }

  /**
   * Getter for server interface. Setting done only through constructor.
   * 
   * @return the serverInterface
   */
  public String getServerInterface() {
    return serverInterface;
  }

//  /**
//   * Reads TraffoPackets out of an input stream. Output stream is currently ignored, probably
//   * should send some status code in the future.
//   * 
//   * @param input InputStream from the remote client.
//   * @param ouput OutputStream to the remote client.
//   * @return TraffoPacket sent by the remote client, or null if the packet received is not a
//   * TraffoPacket.
//   */
//  protected TraffoPacket receivePacket(InputStream input, OutputStream output) {
//    int theByte;
//    boolean isTraffoPacket = false;
//    byte[] packetBytes;
//    int packetLength = 0;
//
//    System.out.println("Got inside receivePacket()"); //DEBUG
//
//    if ((input != null) && (output != null)) {
//      try {
//        theByte = input.read();
//        if (theByte != Packet.HEADER_BASIC) {
//          return null;
//        }
//        System.out.println("Got header byte"); //DEBUG
//        theByte = input.read();
//        switch (theByte) {
//        case Packet.PKT_CLIENT_ACCOUNT_ID:
//          System.out.println("Got Account ID packet type"); // DEBUG
//          break;
//        case Packet.PKT_CLIENT_DEVICE_ID:
//          System.out.println("Got Device ID packet type"); // DEBUG
//          break;
//        case Packet.PKT_CLIENT_CUSTOM_FORMAT_0:
//          System.out.println("Got TraffoPacket packet type"); // DEBUG
//          isTraffoPacket = true;
//          break;
//        }
//          
//        packetLength = input.read();
//        packetBytes = new byte[packetLength];
//
//        for (int i = 0; i < packetLength; i++) {
//          theByte = input.read();
//          if (theByte == -1) {
//            return null;
//          }
//          packetBytes[i] = (byte) theByte;
//        }
//        if (isTraffoPacket) {
//          return new TraffoPacket(packetBytes);          
//        }
//        else
//          return null;
//      }
//      catch (IOException ioe) {
//        Log.error(LOG_NAME, "Unable to read bytes");
//        return null;
//      }
//    }
//    else {
//      Log.error(LOG_NAME, "Null socket/data");
//      return null;
//    }
//  }
}
